1 module hip.api.data.font;
2 import hip.api.data.asset;
3 public import hip.api.renderer.texture;
4 
5 
6 alias HipCharKerning = int[dchar];
7 alias HipFontKerning = HipCharKerning[dchar];
8 ///see hip.graphics.g2d.textrenderer
9 struct HipTextRendererVertexAPI
10 {
11     float[3] vPosition = [0,0,0];
12     float[2] vTexST = [0,0];
13 }
14 
15 /** 
16  *  A text information that is returned from the word wrap range.
17  *  Beyond the information with line and width, it also has a cache.
18  *  This cache is used for optimization in both kerning and associative array
19  *  lookup. This can make a big difference by having a single lookup instead of
20  *  2. The lookup is the slowest part of text rendering, which makes this a lot faster.
21  */
22 pragma(LDC_no_typeinfo)
23 struct HipLineInfo
24 {
25     string line;
26     int height;
27     int width;
28     int minYOffset;
29     const(HipFontChar)*[512] fontCharCache = void;
30     int[512] kerningCache = void;
31 }
32 
33 pragma(LDC_no_typeinfo)
34 struct HipWordWrapRange
35 {
36     private string inputText;
37     private IHipFont font;
38     private int maxWidth, currIndex;
39     private HipLineInfo currLine = void;
40     private bool hasFinished;
41 
42     void initialize(string inputText, IHipFont font, int maxWidth) @nogc
43     {
44         this.inputText = inputText;
45         currLine.height = 0;
46         currLine.width = 0;
47         currLine.minYOffset = 0;
48         this.font = font;
49         this.maxWidth = maxWidth <= 0 ? int.max : maxWidth;
50         currIndex = 0;
51         hasFinished = false;
52     }
53 
54     bool empty() @nogc {return hasFinished;}
55     /** 
56      * Every time it pops the front, it will search for words. Words are defined as text delimited
57      * by spaces. If a word is bigger than the max width, it will be cutten and the word will spam
58      * through multiple lines. 
59      */
60     void popFront() @nogc
61     {
62         const(HipFontChar)* ch, next;
63         int currWidth = 0, wordWidth = 0, wordStartIndex = currIndex;
64         uint spaceWidth = font.spaceWidth;
65 
66         for(int i = currIndex, it = 0; i < inputText.length; i++, it++)
67         {
68             if(ch is null)
69             {
70                 ch = inputText[i] in font.characters;
71                 if(ch is null)
72                 {
73                     currLine.kerningCache[it] = 0;
74                     currLine.fontCharCache[it] = null;
75                     continue;
76                 }
77             }
78             currLine.height = ch.height > currLine.height ?  ch.height : currLine.height;
79             currLine.minYOffset = ch.yoffset < currLine.minYOffset ? ch.yoffset : currLine.minYOffset;
80             int kern = 0;
81             if(i + 1 < inputText.length)
82             {
83                 next = inputText[i+1] in font.characters;
84                 if(next) kern = font.getKerning(ch, next);
85             }
86             currLine.kerningCache[it] = kern;
87             currLine.fontCharCache[it] = ch;
88             switch(inputText[i])
89             {
90                 case '\n':
91                     currLine.line = inputText[currIndex..i];
92                     currLine.width = currWidth + wordWidth;
93                     wordWidth = 0;
94                     wordStartIndex = i+1;
95                     currIndex = i+1;
96                     return;
97                 case ' ':
98                     if(spaceWidth + wordWidth + currWidth > maxWidth)
99                     {
100                         currLine.line = inputText[currIndex..i];
101                         currLine.width = currWidth+wordWidth;
102                         currIndex = i+1;
103                         return;
104                     }
105                     else
106                     {
107                         currWidth+= wordWidth + spaceWidth;
108                         wordStartIndex = i;
109                         wordWidth = 0;
110                     }
111                     break;
112                 default:
113                     if(wordWidth + ch.xadvance + kern + currWidth > maxWidth)
114                     {
115                         if(wordStartIndex == currIndex)
116                         {
117                             currWidth = wordWidth;
118                             wordStartIndex = i;
119                         }
120                         ///Subtract one for ignoring the space.
121                         currLine.line = inputText[currIndex..wordStartIndex];
122                         currLine.width = currWidth;
123                         if(wordStartIndex < inputText.length && inputText[wordStartIndex] == ' ')
124                             wordStartIndex++;
125                         currIndex = wordStartIndex;
126                         return;
127                     }
128                     wordWidth += ch.xadvance + kern;
129                     break;
130             }
131             ch = next;
132         }
133         if(currIndex < inputText.length && inputText[currIndex] == ' ')
134             currIndex++;
135         currLine.line = inputText[currIndex..$];
136         currLine.width = currWidth+wordWidth;
137         currIndex = cast(int)inputText.length;
138         if(currLine.line.length == 0) hasFinished = true;
139     }
140 
141     HipLineInfo front() @nogc
142     {
143         if(currIndex == 0) popFront();
144         return currLine;
145     }
146 }
147 
148 struct HipFontChar
149 {
150     uint id;
151     ///Those are in absolute values
152     int x, y, width, height;
153 
154     int xoffset, yoffset, xadvance, page, chnl;
155 
156     ///Normalized values
157     float normalizedX, normalizedY, normalizedWidth, normalizedHeight;
158     int glyphIndex;
159     void putCharacterQuad(float x, float y, float depth, HipTextRendererVertexAPI[] quad, float scale = 1) const @nogc
160     {
161         import hip.util.data_structures;
162         float w = width*scale, h = height*scale;
163         //Gen vertices 
164         quad[0..4] = [
165             //Top left
166             HipTextRendererVertexAPI(
167                 [x, y, depth],
168                 [normalizedX, normalizedY] //ST
169             ),
170             //Top Right
171             HipTextRendererVertexAPI(
172                 [x+w, y,depth],
173                 [normalizedX + normalizedWidth, normalizedY] //S + Wnorm, T
174             ),
175             //Bot right
176             HipTextRendererVertexAPI(
177                 [x+ w, y +h, depth],
178                 [
179                     normalizedX + normalizedWidth, //S+Wnorm
180                     normalizedY + normalizedHeight //T+Hnorm
181                 ]
182             ),
183             //Bot left
184             HipTextRendererVertexAPI(
185                 [x, y + h, depth],
186                 [normalizedX, normalizedY + normalizedHeight] // S, T+Hnorm
187             )
188         ].staticArray;
189 
190     }
191 }
192 
193 interface IHipFont
194 {
195     int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const @nogc;
196     int getKerning(dchar current, dchar next) const @nogc;
197     uint getHeight() const @nogc;
198     /** 
199      * 
200      * Params:
201      *   text = The text
202      *   linesWidths = Save width per line
203      *   biggestWidth = The biggest width in lines
204      *   height = Height of all the lines together
205      *   maxWidth = If maxWidth != -1, it will break the text into lines automatically. 
206      */
207     void calculateTextBounds(in string text, ref uint[] linesWidths, out int biggestWidth, out int height, int maxWidth = -1) const;
208     /**
209      *
210      * Params:
211      *   text = Input text
212      * Returns: 0 if there is no line break is being done
213      */
214     final uint getTextHeight(in string text)
215     {
216         import hip.util.string;
217         return count(text, "\n") * lineBreakHeight;
218     }
219     HipWordWrapRange wordWrapRange(string text, int maxWidth) const @nogc;
220     ref HipFontChar[dchar] characters() @nogc;
221     ref inout(IHipTexture) texture() inout @nogc;
222     uint spaceWidth() const @nogc;
223     uint spaceWidth(uint newWidth) @nogc;
224 
225     ///Used for reference as height for text
226     uint lineBreakHeight() const @nogc;
227     uint lineBreakHeight(uint newHeight) @nogc;
228 
229 }
230 
231 abstract class HipFont : HipAsset, IHipFont
232 {
233     
234     abstract int getKerning(dchar current, dchar next) const;
235     abstract int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const;
236 
237     this()
238     {
239         super("Font");
240     }
241 
242     ///Underlying GPU texture
243     IHipTexture _texture;
244     HipFontChar[dchar] _characters;
245     ///Saves the space width for the bitmap text process the ' '. If the original spaceWidth is == 0, it won't draw a quad
246     uint _spaceWidth;
247     ///How much the line break will offset in Y the next char
248     uint _lineBreakHeight;
249 
250     ///////Properties///////
251     final ref HipFontChar[dchar] characters(){return _characters;}
252     final ref const(HipFontChar[dchar]) characters() const {return _characters;}
253     final ref inout(IHipTexture) texture() inout {return _texture;}
254     final uint spaceWidth() const {return _spaceWidth;}
255     final uint spaceWidth(uint newWidth){return _spaceWidth = newWidth;}
256     final uint lineBreakHeight() const {return _lineBreakHeight;}
257     final uint lineBreakHeight(uint newHeight){return _lineBreakHeight = newHeight;}
258 
259 
260     abstract uint getHeight() const;
261 
262     final HipWordWrapRange wordWrapRange(string text, int maxWidth) const @nogc
263     {
264         ///Needs to be returned like that or else, it will memset everytime
265         HipWordWrapRange ret = void;
266         ret.initialize(text, cast(IHipFont)this, maxWidth);
267         return ret;
268     }
269     
270 
271     final void calculateTextBounds(string text, ref uint[] linesWidths, out int biggestWidth, out int height, int maxWidth = -1) const
272     {
273         int i = 0;
274         foreach(HipLineInfo lineInfo; wordWrapRange(text, maxWidth))
275         {
276             if(lineInfo.width > biggestWidth) biggestWidth = lineInfo.width;
277             if(linesWidths.length < i+1)
278                 linesWidths.length++;
279             linesWidths[i++] = lineInfo.width;
280         }
281         height = lineBreakHeight*i;
282     }
283     HipFont getFontWithSize(uint size);
284 
285     override void onFinishLoading(){}
286     override void onDispose(){}
287     override bool isReady() const {return _texture !is null;}
288 
289 }